/*
 * PropertyChangedAspect.java
 *
 * Created on 08. Oktober 2007, 22:33
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package net.entelijan.cobean.bind.impl;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

import net.entelijan.cobean.bind.IModelChangeListener;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;

/**
 * 
 * @author wwagner4
 */
class PropertyChangedAdvice implements MethodInterceptor,
		IModelChangeListenersAware {

	private static Log log = LogFactory.getLog(PropertyChangedAdvice.class);

	private List<IModelChangeListener> modelChangeListeners;

	/** Creates a new instance of PropertyChangedAspect */
	public PropertyChangedAdvice() {
		super();
	}

	public Object invoke(final MethodInvocation invoc) throws Throwable {
		Object re;
		String methodName = invoc.getMethod().getName();
		log.debug("[invoke] method:'" + methodName + "' of '" + invoc.getThis()
				+ "' arg:'" + Arrays.toString(invoc.getArguments()) + "'");
		if (methodName.startsWith("set") || methodName.startsWith("add")
				|| methodName.startsWith("remove")) {
			Object val = getValue(invoc);
			Object arg = getArgument(0, invoc);
			for (IModelChangeListener modelChangeListener : this.modelChangeListeners) {
				modelChangeListener.prepareComponent(val, arg);
			}
			re = invoc.proceed();
			// Update the component if the old value could not be retrieved or
			// if it is
			// not equal to the new value.
			if (!equals(val, arg)) {
				PropertyDescriptor propDesc = BeanUtils
						.findPropertyForMethod(invoc.getMethod());
				if (log.isDebugEnabled()) {
					log.debug("[invoke] update "
							+ this.modelChangeListeners.size()
							+ " listeners because value was changed from '"
							+ val + "' to '" + arg + "'");
				}
				int count = 1;
				for (IModelChangeListener modelChangeListener : this.modelChangeListeners) {
					if (propDesc != null) {
						// Only update the components of the property that was
						// changed
						if (modelChangeListener.getPropertyName().equals(
								propDesc.getName())) {
							log.debug("[invoke] update " + count++
									+ " component of modelChangeListener='"
									+ modelChangeListener + "'");
							modelChangeListener.updateComponent();
						}
					} else { // This is not a property but an method like 'add'
								// or 'remove'
						log.debug("[invoke] update " + count++
								+ " component of modelChangeListener='"
								+ modelChangeListener + "'");
						modelChangeListener.updateComponent();
					}
				}
			} else if (log.isDebugEnabled()) {
				log.debug("[invoke] not updating "
						+ this.modelChangeListeners.size() + " components of "
						+ invoc.getThis() + " because old value (" + val
						+ ") equals new value (" + arg + ")");
			}
		} else {
			throw new IllegalStateException(
					"[invoke] The advice should not be called for '"
							+ methodName
							+ "'. The advisor should permit only setters, add and remove");
		}
		return re;
	}

	private boolean equals(Object o1, Object o2) {
		boolean re = false;
		if (o1 == null && o2 == null) {
			re = true;
		} else if (o1 != null && o2 != null) {
			re = o1.equals(o2);
		}
		return re;
	}

	private Object getArgument(int index, MethodInvocation invoc) {
		Object re = null;
		Object[] arguments = invoc.getArguments();
		if (arguments != null && arguments.length > index) {
			re = arguments[index];
		}
		return re;
	}

	/**
	 * Gets the value if the method was a setter. In every other case null is
	 * returned. That means new and old value cannot be compared. In that case
	 * the component gets also updated if the value was not changed.
	 * 
	 */
	private Object getValue(MethodInvocation invoc) {
		Object re = null;
		String setterName = invoc.getMethod().getName();
		String getterName = getterNameFromSetterName(setterName);
		log.debug("[getValue] setterName='" + setterName + "' getterName='"
				+ getterName + "'");
		try {
			Method getter = findMethod(invoc.getThis().getClass(), getterName);
			if (getter != null) {
				re = getter.invoke(invoc.getThis(), new Object[0]);
			}
		} catch (IllegalArgumentException e) {
			// nothing to be done. return null
		} catch (IllegalAccessException e) {
			// nothing to be done. return null
		} catch (InvocationTargetException e) {
			// nothing to be done. return null
		}
		log.debug("[getValue] re:'" + re + "'");
		return re;
	}

	private String getterNameFromSetterName(String string) {
		return "get" + string.substring(3);
	}

	private Method findMethod(Class<?> clazz, String name) {
		Method ret = null;
		for (int i = 0; i < clazz.getMethods().length; i++) {
			Method m = clazz.getMethods()[i];
			if (m.getName().equals(name) && m.getParameterTypes().length == 0) {
				ret = m;
				break;
			}
		}
		return ret;
	}

	public List<IModelChangeListener> getModelChangeListeners() {
		return this.modelChangeListeners;
	}

	public void setModelChangeListeners(
			List<IModelChangeListener> modelChangeListeners) {
		this.modelChangeListeners = modelChangeListeners;
	}

}
